package com.atguigu.srb.core.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.srb.base.asserts.Asserts;
import com.atguigu.srb.base.consts.SrbConsts;
import com.atguigu.srb.base.exception.BusinessException;
import com.atguigu.srb.base.response.ResponseEnum;
import com.atguigu.srb.base.util.IpUtils;
import com.atguigu.srb.base.util.JwtUtils;
import com.atguigu.srb.common.util.HttpClientUtils;
import com.atguigu.srb.common.util.MD5;
import com.atguigu.srb.common.util.RegexValidateUtils;
import com.atguigu.srb.core.config.WxLoginProperties;
import com.atguigu.srb.core.pojo.entity.UserInfo;
import com.atguigu.srb.core.mapper.UserInfoMapper;
import com.atguigu.srb.core.pojo.entity.UserLoginRecord;
import com.atguigu.srb.core.pojo.entity.vo.LoginVO;
import com.atguigu.srb.core.pojo.entity.vo.RegisterVO;
import com.atguigu.srb.core.pojo.entity.vo.UserInfoIndexVO;
import com.atguigu.srb.core.pojo.entity.vo.UserInfoSearchVO;
import com.atguigu.srb.core.service.UserInfoService;
import com.atguigu.srb.core.service.UserLoginRecordService;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.ParseException;
import java.util.Map;
import java.util.UUID;

/**
 * <p>
 * 用户基本信息 服务实现类
 * </p>
 *
 * @author Atguigu
 * @since 2023-05-26
 */
@Service
@Slf4j
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
    @Resource
    StringRedisTemplate stringRedisTemplate;
    @Resource
    UserLoginRecordService userLoginRecordService;
    @Override
    public void saveUser(RegisterVO registerVO) {
        //1、验证参数：手机号码格式、密码不能为空
        String mobile = registerVO.getMobile();
        String code = registerVO.getCode();
        String password = registerVO.getPassword();
        Integer userType = registerVO.getUserType();
        //手机号不能为空 且满足正则要求
        Asserts.assertNotTrue(!RegexValidateUtils.checkCellphone(mobile),
                ResponseEnum.MOBILE_ERROR);
        //code和userType不能为空
        Asserts.assertNotNull(code,ResponseEnum.CODE_ERROR);
        Asserts.assertNotNull(userType,ResponseEnum.PARAM_ERROR);
        //密码不能为空
        Asserts.assertNotNull(password,ResponseEnum.PASSWORD_NULL_ERROR);

        //2、校验验证码：  验证码必须和redis中缓存的验证码一致
        //获取注册时的手机号码在redis中缓存的验证码 和前端用户提交的验证码比较
        String redisCodeKey = SrbConsts.SEND_CODE_PREFIX+mobile;
        String redisCode = stringRedisTemplate.opsForValue().get(redisCodeKey);
        //redisCode为空 或者 redisCode不等于code
        Asserts.assertNotTrue(StringUtils.isEmpty(redisCode)||!StringUtils.equals(redisCode,code),
                ResponseEnum.CODE_ERROR);
        //验证码正确：删除redis中的缓存
        stringRedisTemplate.delete(redisCodeKey);
        //3、手机号重复注册校验: 发送短信时也会校验
        Asserts.assertNotTrue(this.count(Wrappers.lambdaQuery(UserInfo.class)
                .eq(UserInfo::getMobile , mobile))>0,ResponseEnum.MOBILE_EXIST_ERROR);
        //4、密码加密： 给密码加盐(防止密码被暴力破解)
        //用户表中一般有salt字段+密码字段   salt保存盐字符串  ， 密码保存加盐后的加密结果
        //4.1 生成盐
        String salt = UUID.randomUUID().toString().substring(0,6);
        //4.2 使用MD5 用盐对密码加密
        String encrypt = MD5.encrypt(MD5.encrypt(password) + salt);
        //5、将注册数据转为UserInfo对象 保存到数据库
        UserInfo userInfo = new UserInfo();
        userInfo.setUserType(userType);
        //默认头像
        userInfo.setHeadImg("https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif");
        userInfo.setMobile(mobile);
        //使用加密后的结果
        userInfo.setPassword(encrypt);
        userInfo.setSalt(salt);
        //初始化积分：0
        userInfo.setIntegral(0);
        userInfo.setNickName("宝粉_"+UUID.randomUUID().toString().substring(0,8));
        userInfo.setStatus(1);

        this.save(userInfo);
    }

    @Override
    public String login(LoginVO loginVO, HttpServletRequest request) {
        //1、根据账号查询用户信息
        String mobile = loginVO.getMobile();
        Integer userType = loginVO.getUserType();
        String password = loginVO.getPassword();
        //手机号不能为空 且满足正则要求
        Asserts.assertNotTrue(!RegexValidateUtils.checkCellphone(mobile),
                ResponseEnum.MOBILE_ERROR);
        //code和userType不能为空
        Asserts.assertNotNull(userType,ResponseEnum.PARAM_ERROR);
        //密码不能为空
        Asserts.assertNotNull(password,ResponseEnum.PASSWORD_NULL_ERROR);
        //getOne底层使用的是mybatis的selectOne，查询到的结果集必须为空或者一条
        UserInfo userInfo = this.getOne(Wrappers.lambdaQuery(UserInfo.class)
                .eq(UserInfo::getMobile, mobile)
                .last("limit 0,1"));
        //未查询到 账号错误
        Asserts.assertNotNull(userInfo , ResponseEnum.LOGIN_MOBILE_ERROR );
        //查询到 但是和注册时的用户类型不一致
        //包装类必须使用 equals比较(-127-128间的数字有缓存)
        Asserts.assertNotTrue(userInfo.getUserType().intValue()!=userType.intValue()
                , ResponseEnum.LOGIN_USER_TYPE_ERROR);

        //2、如果查询到用户，再校验登录密码是否正确
        //使用登录的密码 参考注册时的加密方式加密 和 查询到数据库用户密码比较
        String salt = userInfo.getSalt();
        String encryptPwd = MD5.encrypt(MD5.encrypt(password) + salt);
        Asserts.assertNotTrue(!userInfo.getPassword().equals(encryptPwd)
                , ResponseEnum.LOGIN_PASSWORD_ERROR);
        //3、将用户信息转为jwt的token响应
//        Jwts.builder()
                //头： 配置签名加密算法、token的格式
                //载荷: 系统载荷-> 过期时间、主题      自定义载荷-> 登录成功我们希望自定义设置的数据
                //签名： 根据载荷内容使用签名加密算法 + 后端服务器保存的唯一的key 加密得到的内容 防止jwt的token被篡改
        String token = JwtUtils.createToken(userInfo.getId(), userInfo.getNickName());
        //登录成功： 保存登录日志到user_login_record表中
        UserLoginRecord userLoginRecord = new UserLoginRecord();
        //日志绑定会员id
        userLoginRecord.setUserId(userInfo.getId());
        String ip = IpUtils.getIpAddressAtService(request);
        // 192.168.1.1
        //登录请求提交给了nginx，nginx反向代理时如果没有配置默认使用自己的ip当做客户端ip来转发请求到后端项目
        userLoginRecord.setIp(ip);
        userLoginRecordService.save(userLoginRecord);
        return token;
    }

    @Override
    public String wxLogin(HttpSession session) {

        try {
            String url = "https://open.weixin.qq.com/connect/qrconnect?" +
                    "appid=%s" +
                    "&redirect_uri=%s" +
                    "&response_type=code" +
                    "&scope=snsapi_login" +
                    "&state=%s" +
                    "#wechat_redirect";
            //防止乱码
            String redirectUri = URLEncoder.encode(WxLoginProperties.REDIRECT_URI,"UTF-8");
            String state = UUID.randomUUID().toString().replace("-","");
            //缓存state到服务器内存中：回调接口中需要获取使用
            session.setAttribute("wxState",state);
            url = String.format(url, WxLoginProperties.APP_ID,redirectUri,state);
            return "redirect:"+url;

        } catch (UnsupportedEncodingException e) {
            throw new BusinessException(ResponseEnum.ERROR);
        }

    }

    @Override
    public String callback(String code, String state, HttpServletRequest request,
                           HttpSession session) {
        //校验回调传入的state是否合法
        Object wxState = session.getAttribute("wxState");
        Asserts.assertNotTrue(wxState==null||!wxState.equals(state),
                ResponseEnum.WEIXIN_CALLBACK_PARAM_ERROR);
        session.removeAttribute("wxState");
        try {
            //1、获取code和state的值
            //code代表授权的wx用户的code值
            //state代表上一步生成的唯一字符串，防止恶意攻击
            //TODO：2、校验state值
            //3、通过code获取access_token
            String url = "https://api.weixin.qq.com/sns/oauth2/access_token" +
                    "?appid="+ WxLoginProperties.APP_ID +
                    "&secret=" + WxLoginProperties.APP_SECRET+
                    "&code="+ code +
                    "&grant_type=authorization_code";
            //发起网络请求访问该接口就可以得到响应：
            HttpClientUtils client = new HttpClientUtils(url);
            //发起get请求
            client.get();
            //获取响应结果
            String content = client.getContent();
            log.info("获取accessToken:{}",content);
            //解析响应结果
            Map<String,Object> result = JSON.parseObject(content, Map.class);
            //判断如果wx返回的是错误抛出异常
            if(result.get("errcode")!=null){
                log.error("获取accessToken失败：{}",result);
                throw new BusinessException(Integer.parseInt(result.get("errcode").toString()),
                        result.get("errmsg").toString());
            }
            //获取accessToken成功: 获取accessToken、openid
            String accessToken = result.get("access_token").toString();
            //Openid代表wx用户的唯一标识
            String openid = result.get("openid").toString();
            //判断如果数据库中已存在openid的wx用户，直接查询返回
            UserInfo userInfo = this.getOne(Wrappers.lambdaQuery(UserInfo.class)
                    .eq(UserInfo::getOpenid, openid));
            if(userInfo==null || ((System.currentTimeMillis()-userInfo.getUpdateTime().getTime())>=3*24*60*60*1000) ) {
                //4、通过access_token获取wx用户的个人信息
                url = "https://api.weixin.qq.com/sns/userinfo" +
                        "?access_token=" + accessToken +
                        "&openid=" + openid;
                client = new HttpClientUtils(url);
                client.get();
                content = client.getContent();
                result = JSON.parseObject(content, Map.class);
                //判断获取是否成功
                if (result.containsKey("errcode")) {
                    log.error("获取用户个人信息失败：{}", content);
                    throw new BusinessException(Integer.parseInt(result.get("errcode").toString()),
                            result.get("errmsg").toString());
                }
                //获取成功：解析用户数据
                String nickname = result.get("nickname").toString();
                String headimgurl = result.get("headimgurl").toString();
                if(userInfo==null){
                    //5、将wx用户转为我们的用户：不需要密码，openid代表唯一的一个wx用户
                    userInfo = new UserInfo();
                    //表示用户身份未知，以后需要让他设置
                    userInfo.setUserType(0);
                    userInfo.setIntegral(0);
                    userInfo.setOpenid(openid);
                    userInfo.setHeadImg(headimgurl);
                    userInfo.setNickName(nickname);
                    this.save(userInfo);
                }else{
                    //更新 wx用户的数据
                    userInfo.setHeadImg(headimgurl);
                    userInfo.setNickName(nickname);
                    this.updateById(userInfo);
                }

            }
            //userInfo在本地数据库中已经存在
            //判断当前时间和wx用户保存到数据库的时间是否超过3天
            //wx用户如果数据库存储的数据的时间和现在时间差超过3天，我们更新数据库中该用户的wx数据
            //6、将wx用户的数据封装为jwt的token 跳转到前端项目的登录成功页面(首页)
            String token = JwtUtils.createToken(userInfo.getId(), userInfo.getNickName());
            //保存wx登录日志：
            UserLoginRecord userLoginRecord = new UserLoginRecord();
            userLoginRecord.setIp(IpUtils.getIpAddressAtService(request));
            userLoginRecord.setUserId(userInfo.getId());
            userLoginRecordService.save(userLoginRecord);
            //前端获取到token后 回显信息时再根据token访问后端接口 查询数据库中id对应的用户的数据显示
            return "redirect:http://localhost:3000?token="+token;
        } catch (Exception e) {
            throw new BusinessException(ResponseEnum.WEIXIN_CALLBACK_PARAM_ERROR);
        }
    }

    @Override
    public Page<UserInfo> searchUserInfos(Integer pageNum, Integer pageSize, UserInfoSearchVO userInfoSearchVO) {
        Page<UserInfo> page = new Page<>(pageNum,pageSize);
        //this.page方法查询到分页数据集合后会自动设置给 page对象的records属性
        LambdaQueryWrapper<UserInfo> wrapper = Wrappers.lambdaQuery(UserInfo.class);
        //设置查询条件
        String mobile = userInfoSearchVO.getMobile();
        Integer userType = userInfoSearchVO.getUserType();
        Integer status = userInfoSearchVO.getStatus();
        if(StringUtils.isNotEmpty(mobile)){
            //根据手机号模糊查询:  mobile like '%mobile%'
            wrapper.like(UserInfo::getMobile , mobile);
        }
        if(userType!=null){
            //使用用户类型等值查询
            wrapper.eq(UserInfo::getUserType , userType);
        }
        if(status!=null){
            wrapper.eq(UserInfo::getStatus , status);
        }
        this.page(page,wrapper);
        return page;
    }

    @Override
    public UserInfoIndexVO getUserInfoIndexVO(String token) {
        //1、获取userId
        Long userId = JwtUtils.getUserId(token);
        //2、查询需要的数据
        UserInfoIndexVO userInfoIndexVO = baseMapper.selectUserInfoIndexVOByUserId(userId);
        return userInfoIndexVO;
    }

    @Override
    public UserInfo updateUserInfo(String token, String mobile, Integer userType) {
        Long userId = JwtUtils.getUserId(token);
        Asserts.assertNotTrue(!(RegexValidateUtils.checkCellphone(mobile)
                        && (userType==1 || userType==2)),
                ResponseEnum.PARAM_ERROR);
        // update table set 字段=值 where 条件
        this.update(Wrappers.lambdaUpdate(UserInfo.class)
                .eq(UserInfo::getId , userId)
                .set(UserInfo::getMobile,mobile)
                .set(UserInfo::getUserType ,userType));
        //返回更新后的userInfo数据，前端页面可以在更新后直接回显(判断用户类型 不显示对话框)
        UserInfo userInfo = this.getOne(Wrappers.lambdaQuery(UserInfo.class)
                .eq(UserInfo::getId , userId)
                .select(UserInfo::getId,
                        UserInfo::getUserType,
                        UserInfo::getHeadImg,
                        UserInfo::getNickName));
        return userInfo;
    }

    @Override
    public UserInfo checkAccount(String token) {
        //借款人提交借款申请时：账号必须正常、必须已绑定hfb账户、借款人申请状态已审核通过
        Long userId = JwtUtils.getUserId(token);
        return checkAccount(userId);
    }

    @Override
    public UserInfo checkAccount(Long id) {
        UserInfo userInfo = this.getById(id);
        Integer status = userInfo.getStatus();
        Integer bindStatus = userInfo.getBindStatus();
        Integer borrowAuthStatus = userInfo.getBorrowAuthStatus();
        //用户账户是否锁定
        Asserts.assertNotTrue(status!=1 , ResponseEnum.LOGIN_LOKED_ERROR);
        //hfb账户是否绑定
        Asserts.assertNotTrue(bindStatus!=1 , ResponseEnum.USER_NO_BIND_ERROR);
        if(userInfo.getUserType()==2){
            //借款人申请状态是否已审核通过
            Asserts.assertNotTrue(borrowAuthStatus!=2 , ResponseEnum.USER_NO_AMOUNT_ERROR);
        }

        return userInfo;
    }
}
